use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use syn::{Data::Struct, DataStruct, DeriveInput, Fields::{Named, Unnamed}, FieldsNamed, FieldsUnnamed};

mod fields;

use crate::fields::{
    builder_field_definitions, builder_init_values, builder_methods, original_struct_setters,
};

pub fn create_builder(item: TokenStream) -> TokenStream {
    let ast: DeriveInput = syn::parse2(item).unwrap();
    let name = ast.ident;
    let builder = format_ident!("{}Builder", name);

    let (named, fields) = match ast.data {
        Struct(DataStruct {
            fields: Named(FieldsNamed { ref named, .. }),
            ..
        }) => (true, named),
        Struct(DataStruct {
            fields: Unnamed(FieldsUnnamed { ref unnamed, .. }),
            ..
        }) => (false, unnamed),
        _ => unimplemented!("only implemented for structs"),
    };

    let builder_fields = builder_field_definitions(fields);
    let builder_inits = builder_init_values(fields);
    let builder_methods = builder_methods(fields);
    let set_fields = original_struct_setters(fields);

    if named {
        quote! {
            struct #builder {
                #(#builder_fields,)*
            }
            impl #builder {
                #(#builder_methods)*

                pub fn build(self) -> #name {
                    #name {
                        #(#set_fields,)*
                    }
                }
            }
            impl #name {
                pub fn builder() -> #builder {
                    #builder {
                        #(#builder_inits,)*
                    }
                }
            }
        }
    } else {
        quote! {
            struct #builder(#(#builder_fields, )*);

            impl #builder {
                #(#builder_methods)*

                pub fn build(self) -> #name {
                    #name(
                        #(#set_fields,)*
                    )
                }
            }
            impl #name {
                pub fn builder() -> #builder {
                    #builder (
                        #(#builder_inits,)*
                    )
                }
            }
        }
    
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn builder_struct_name_should_be_present_in_output() {
        let input = quote! {
            struct StructWithNoFields {}
        };

        let actual = create_builder(input);

        assert!(actual.to_string().contains("StructWithNoFieldsBuilder"));
    }

    #[test]
    fn builder_struct_with_expected_methods_should_be_present_in_output() {
        let input = quote! {
            struct StructWithNoFields {}
        };
        let expected = quote! {
            struct StructWithNoFieldsBuilder {}
        };

        let actual = create_builder(input);

        assert_eq!(actual.to_string(), expected.to_string());
    }

    #[test]
    fn assert_with_parsing() {
        let input = quote! {
            struct StructWithNoFields {}
        };

        let actual = create_builder(input);

        let derived: DeriveInput = syn::parse2(actual).unwrap();
        let name = derived.ident;
        assert_eq!(name.to_string(), "StructWithNoFieldsBuilder");
    }
}
